#include <jni.h>
#include <string>
#include <vector>
#include <thread>
#include <future>
#include <map>
#include <chrono>
#include <pthread.h>
#include "android_log.h"

typedef std::map<std::string, std::pair<std::thread, std::shared_future<int>>> map_type;
map_type cmd_maps;

extern "C" int run_ffmpeg_command(int argc, char **argv);

#define MAX_PARAM_COUNT 100

extern "C"
void exit_thread(int ret) {
    throw ret;
}


int put_new_param(int argc, char **argv, const char *start, const char *to) {
    if (argc + 1 >= MAX_PARAM_COUNT) {
        return -2;
    }
    if (*(to-1) == '"') {
        to--;
    }
    size_t len = to - start + 1;
    argv[argc] = new char[len];
    logi("\"%s\"", start);
    memcpy(argv[argc], start, len - 1);
    argv[argc][len - 1] = '\0';
    return ++argc;
}

/**
 * from https://stackoverflow.com/questions/26187037/in-c-split-char-on-spaces-with-strtok-function-except-if-between-quotes
 * @param cmd
 * @param argv_output argv output
 * @return argc
 */
int split_space_not_quote(std::string cmd, char ***argv_output) {
    char *cmdBuf = new char[cmd.length() + 1];
    char *cmdOriginBuf = cmdBuf;
    strcpy(cmdBuf, cmd.data());

    const char *start = cmdBuf;
    int state = ' ';
    int argc = 0;
    char **argv = new char*[MAX_PARAM_COUNT];
    while (*cmdBuf) {
        switch (state) {
            case '\n': // Could add various white-space here like \f \t \r \v
            case ' ': // Consuming spaces
                if (*cmdBuf == '\"') {
                    cmdBuf++;
                    start = cmdBuf;
                    state = '\"';  // begin quote
                } else if (*cmdBuf != ' ') {
                    start = cmdBuf;
                    state = 'T';
                }
                break;
            case 'T': // non-quoted text
                if (*cmdBuf == ' ') {
                    argc = put_new_param(argc, argv, start, cmdBuf);
                    state = ' ';
                } else if (*cmdBuf == '\"') {
                    state = '\"'; // begin quote
                }
                break;
            case '\"': // Inside a quote
                if (*cmdBuf == '\"') {
                    state = 'T'; // end quote
                }
                break;
        }
        cmdBuf++;
    } // end while
    if (state != ' ') {
        argc = put_new_param(argc, argv, start, cmdBuf);
    }
    *argv_output = argv;
    delete [] cmdOriginBuf;
    return argc;
}

/**
 *
 * @param cmd
 * @param argv_out
 * @return
 */
int split_command(std::string cmd, char ***argv_out) {
    char *cmdBuf = new char[cmd.length() + 1];
    strcpy(cmdBuf, cmd.data());
    int argc = 0;
    char **argv = new char*[MAX_PARAM_COUNT];
    const char * sep = " \"'";
    char *p;
    p = strtok(cmdBuf, sep);
    while (p != nullptr) {
        if (argc + 1 >= MAX_PARAM_COUNT) {
            return -2;
        }
        argv[argc] = new char[strlen(p) + 1];
        strcpy(argv[argc], p);
        argc++;
        p = strtok(nullptr, sep);
    }
    *argv_out = argv;
    delete[] cmdBuf;
    return argc;
}

int cmd_run_thread(std::string key, std::string cmd) {
    logi("FFmpeg Thread[%s]: %s", key.data(), cmd.data());
    // use strtok to split cmd to arguments
    char **argv = nullptr;
    int argc = split_space_not_quote(cmd, &argv);

//#ifdef DEBUG
    for (int i = 0; i < argc; ++i) {
        logi("argv[%d]: %s", i, argv[i]);
    }
//#endif

    try {
        int ret = run_ffmpeg_command(argc, argv);
        return ret;
    } catch (int ret) {
        return ret;
    }
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_gotokeep_keep_video_KeepVideoUtil_FFmpegRun(JNIEnv *env, jobject instance, jstring tag_,
                                                     jstring cmd_) {
    const char *tag = env->GetStringUTFChars(tag_, 0);
    const char *cmd = env->GetStringUTFChars(cmd_, 0);

    std::string key(tag);
    if (cmd_maps.find(key) != cmd_maps.end()) {
        return -1;
    }

    int result = 0;
    if (strlen(tag) == 0) {

        try {
            result = cmd_run_thread(key, cmd);
        } catch (int ret) {
            result = ret;
        } catch (std::exception &ex) {
            loge("Exception: %s", ex.what());
            result = -1;
        }
    } else {
        std::packaged_task<int(std::string, std::string)> pk_tsk(cmd_run_thread);
        std::shared_future<int> tsk_future = pk_tsk.get_future();
        std::thread cmd_thread(std::move(pk_tsk), key, std::string(cmd));
        cmd_maps[key] = std::move(std::make_pair(std::move(cmd_thread), tsk_future));
    }

    env->ReleaseStringUTFChars(tag_, tag);
    env->ReleaseStringUTFChars(cmd_, cmd);
    return result;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_gotokeep_keep_video_KeepVideoUtil_FFmpegKill(JNIEnv *env, jobject instance, jstring tag_) {
    const char *tag = env->GetStringUTFChars(tag_, 0);

    std::string key(tag);
    map_type::iterator iter = cmd_maps.find(key);
    if (iter != cmd_maps.end()) {
        std::thread &thread = iter->second.first;
        cmd_maps.erase(iter);
        if (thread.joinable()) {
            std::thread::native_handle_type thread_handle = thread.native_handle();
            thread.detach();
            if (pthread_kill(thread_handle, SIGINT) == 0) {
                loge("Send SIGINT to kill FFmpeg thread [%s] failed", tag);
            }
        }
    }

    env->ReleaseStringUTFChars(tag_, tag);
}

extern "C"
JNIEXPORT jboolean JNICALL
Java_com_gotokeep_keep_video_KeepVideoUtil_FFmpegIsRunning(JNIEnv *env, jobject instance,
                                                           jstring tag_) {
    const char *tag = env->GetStringUTFChars(tag_, 0);
    jboolean result = JNI_FALSE;

    std::string key(tag);
    map_type::iterator iter = cmd_maps.find(key);
    if (iter != cmd_maps.end()) {
        std::thread &thread = iter->second.first;
        std::shared_future<int> tsk_result = iter->second.second;
        std::chrono::milliseconds span(50);
        if (tsk_result.valid() && tsk_result.wait_for(span) == std::future_status::timeout) {
            result = JNI_TRUE;
        }
    }

    env->ReleaseStringUTFChars(tag_, tag);
    return result;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_gotokeep_keep_video_KeepVideoUtil_nativeInit(JNIEnv *env, jclass type) {
    logd("Native Init.");
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_gotokeep_keep_video_KeepVideoUtil_FFmpegResult(JNIEnv *env, jclass type, jstring tag_) {
    const char *tag = env->GetStringUTFChars(tag_, 0);

    // TODO

    env->ReleaseStringUTFChars(tag_, tag);
}